Auswertung von Daten der Deutschen Bahn

Github: https://github.com/lz039/python4ds_project

Einleitung

schienen

Schienennetz vor dem Kollaps - so titeln Marie Blöcher, Nils Naber und Isabel Schneider vom NDR. Die Deutsche Bahn habe zwar ehrgeizige Ziele, allerdings ist jahrelang zu wenig Geld ins Netz geflossen.
Rund 60 Milliarden Euro müssten laut DB ausgegeben werden, um alle Probleme im Netz zu beheben, die sich über die vergangenen Jahre angesammelt haben. Der Zustand von Strecken und Gleisen wurde über viele Jahre vernachlässigt, sagt Bahnexperte Christian Böttger.

Die Bahn steht aktuell in keinem guten Licht. Zu viele Verspätungen, Zugausfälle und marode Infrastruktur.
Doch wie steht es wirklich um den Zustand der Bahn?

In dieser Analyse wird auf Daten der Deutschen Bahn zugegriffen, um dieser Frage auf den Grund zu gehen.
Die Bahn stellt über den API Marketplace eine Fülle an Daten zur Verfügung.

schienen

Die meisten Daten stehen kostenlos zur Verfügung, für einige sind kostenpflichtige Pläne nötig.
Einige APIs liefern ähnliche Funktionalität nur in anderem Format oder noch detaillierter, z.B. RIS::Stations und StaDa. Im Rahmen dieses Projekts werden folgende Daten betrachtet:

  • Als Grundlage dient RIS::Stations, worüber sich alle deutschen Bahnhöfe abrufen lassen.
  • FaSta - Station Facility Status gibt Auskunft über den Zustand der Bahnhöfe
  • Railway-Stations Pictures ermöglich den Zugriff auf Bilder jedes einzelnen Bahnhofs.
  • Facility Stations sind alle Dienstleistungen rund um die Bahnhöfe.

Daten abrufen

Über folgende APIs werden die Daten im JSON-Format abgerufen, in pandas dataframes umgewandelt und gespeichert.

url_parking = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/'
url_ris_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/ris-stations/v1/'
url_rw_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/api.railway-stations.org/photoStationById/'
url_facility_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/stations/'

Häufig können nicht alle Daten auf einmal abgerufen werden, weshalb mehrere Aufrufe gemacht werden müssen. Anschließend ist ein mapping der JSON Bahn-API-Datenstruktur auf ein flaches Data Frame nötig.

Da der Abruf der Daten einige Minuten dauert, ist dieser und die eigentliche Auswertung in zwei verschiedenen files getrennt.

Imports

Es werden Dateien im Pickel-Format genutzt, um ganze Pandas Dataframes zwischen dem Scraping-Prozess und der Datenauswertung auszutauschen.
Der Vorteil dabei ist, dass die Struktur und die Datenformate beibehalten werden.
Für die Visualisierung wird Plotly und Folium genutzt.

Da für gewisse Funktionen Python 3.10.1 benötigt wurde, bringt das Projekt eine eigene virtuelle Python Umgebung mit. Allerdings werden nur die folgenden Pakete benötigt

#!pip install "pandas<2.0.0"
#!pip install plotly
#!pip install folium
#!pip install geopy
#!pip install nbformat 
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

import pickle

import plotly.express as px
import plotly.graph_objects as go
import folium
import folium.plugins as plugins

from geopy.geocoders import Nominatim

Daten laden

Hier werden die Daten aus den Pickel-Files in Dataframes geladen. Da diese Funktionalität häufig benötigt wird, ist sie in eine Funktion ausgelagert. Die Daten aus dem Scraping liegen im data Ordner.

data_folder = 'data/'
def loadData(fileName):
    with open(data_folder + fileName, 'rb') as pkl_file:
        return pickle.load(pkl_file)
df_stations = loadData('stations.pkl')
df_stopplaces = loadData('stopplaces_new.pkl')
df_facilities = loadData('station_facilities.pkl')

Es kann zwar mehrere Bilder pro Bahnhof geben, dies wurde beim data scraping allerdings bereits berücksichtigt, sodass immer genau ein Bild pro Bahnhof vorhanden ist.
Die Bilder sind noch als Dictionary gespeichert, sodass dies noch in ein Pandas DataFrame umgewandelt werden muss.

df_station_images = loadData('station_images.pkl')

df_images = pd.DataFrame.from_dict({k: v for k, v in df_station_images.items() if v}).T
df_images.columns = ['image']
df_images = df_images.reset_index()

Die verschiedenen Daten passen von der Anzahl nicht komplett zusammen.
Es ist aber durchaus erklärbar, dass es mehr Einrichtungen und Haltestellen als tatsächliche Bahnhöfe bzw. Bahnhofsgebäude gibt. Wie genau die Daten aussehen, wird im Folgenden geprüft.

print(f'Stations: {df_stations.shape}')
print(f'Station images: {df_images.shape}')
print(f'Facilities: {df_facilities.shape}')
print(f'Stopplaces: {df_stopplaces.shape}')
Stations: (5690, 16)
Station images: (5627, 2)
Facilities: (3550, 6)
Stopplaces: (5727, 9)

Bahnhöfe

Das Data Frame der Bahnhöfe enthält alle Haltestellen der Deutschen Bahn in Deutschland.
Jeder Bahnhof hat eine eindeutige id. Zusätzlich werden beispielsweise der Name, die Adresse und Geo-Koordinaten mitgeliefert.

df_stations.head(3)
id name metropolis street houseNumber postalCode city state country stationCategory owner organisationalUnit countryCode latitude longitude timeZone
0 1 Aachen Hbf {} Bahnhofstr. 2a 52064 Aachen Nordrhein-Westfalen DE CATEGORY_2 DB S&S RB West DE 50.767800 6.091499 Europe/Berlin
1 1000 Burkhardswalde-Maxen {} Gesundbrunnen 60c 01809 Müglitztal-Burkhardswalde Sachsen DE CATEGORY_7 DB S&S RB Südost DE 50.925146 13.838369 Europe/Berlin
2 1001 Burkhardtsdorf {} Bahnhofstraße NaN 09235 Burkhardtsdorf Sachsen DE CATEGORY_6 DB Regio-Netze Erzgebirgsbahn (EGB) DE NaN NaN Europe/Berlin

Bilder von Bahnhöfen

Nutzer können Bilder zu Bahnhöfen hochladen. Das Data Frame enthält Links zu diesen Bildern. Die Spalte index referenziert die Spalte id der Bahnhöfe.

df_images.head(3)
index image
0 1 https://api.railway-stations.org/photos/de/1_1...
1 1000 https://api.railway-stations.org/photos/de/100...
2 1001 https://api.railway-stations.org/photos/de/100...

Hier ein Beispiel vom Bahnhof in Aachen.

Einrichtungen

Mit Einrichtungen sind beispielsweise Geräte wie Aufzüge auf Bahnhöfen gemeint.
Das interessante ist, dass auch die Funktionstüchtigkeit der Einrichtung mitgeliefert wird.

df_facilities.head(3)
id description operatorname state stateExplanation type
0 1 zu Gleis 1 DB Station&Service ACTIVE available ELEVATOR
1 1 zu Gleis 2/3 DB Station&Service ACTIVE available ELEVATOR
2 1 zu Gleis 6/7 DB Station&Service ACTIVE available ELEVATOR

In Wahrheit gibt es genau zwei verschiedene Arten von Einrichtungen: Aufzüge und Rolltreppen.

df_facilities['type'].unique()
array(['ELEVATOR', 'ESCALATOR'], dtype=object)

Haltestellen

Haltestellen enthalten viele Informationen zu den Bahnhöfen, zusätzlich auch die Art der Transportmittel und welcher Verkehrsbund gilt.

df_stopplaces.head(3)
id name availableTransports transportAssociations countryCode state timeZone latitude longitude
0 1 Aachen Hbf [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] DE NW Europe/Berlin 50.767800 6.091499
1 1000 Burkhardswalde-Maxen [REGIONAL_TRAIN] [VVO] DE SN Europe/Berlin 50.925146 13.838369
2 1001 Burkhardtsdorf [REGIONAL_TRAIN, BUS] [VMS] DE SN Europe/Berlin 50.733196 12.932137

Explorative Datenanalyse

Datentypen

Als erstes wird geprüft, ob die Data Frames korrekte Datentypen haben und werden bei Bedarf korrigiert.
Die Spalte id soll immer vom Typ int sein, um sie später besser zusammenführen zu können.

df_stations['id'] = df_stations['id'].astype(int)
df_stations.dtypes
id                      int32
name                   object
metropolis             object
street                 object
houseNumber            object
postalCode             object
city                   object
state                  object
country                object
stationCategory        object
owner                  object
organisationalUnit     object
countryCode            object
latitude              float64
longitude             float64
timeZone               object
dtype: object
df_stopplaces['id'] = df_stopplaces['id'].astype(int)
df_stopplaces.dtypes
id                         int32
name                      object
availableTransports       object
transportAssociations     object
countryCode               object
state                     object
timeZone                  object
latitude                 float64
longitude                float64
dtype: object
df_facilities['id'] = df_facilities['id'].astype(int)
df_facilities.dtypes
id                   int32
description         object
operatorname        object
state               object
stateExplanation    object
type                object
dtype: object
df_images['index'] = df_images['index'].astype(int)
df_images.dtypes
index     int32
image    object
dtype: object

Fehlende Werte

Als nächstes wird auf fehlende Werte geprüft, um zu prüfen, ob Korrekturen notwendig sind.

df_stations.isna().sum()
id                      0
name                    0
metropolis              0
street                  8
houseNumber           893
postalCode              7
city                    4
state                   0
country                 0
stationCategory        12
owner                   0
organisationalUnit      0
countryCode             0
latitude              282
longitude             282
timeZone                0
dtype: int64

Das Hausnummernfeld fehlt sehr oft. Da diese Information aber nicht genutzt wird, stellt dies kein Problem dar.

Die Angaben für Latitude und Longitude fehlen ebenfalls häufig. Es kann über die Adresse versucht werden, die fehlenden Werte herauszufinden.
Da das Nachschauen der Werte einige Zeit in Anspruch nimmt, ist dieser Code in der finalen Abgabe auskommentiert.

Grundsätzlich wird aber anhand der Spalten postalCode, city, state und country mithilfe des Pakets aus der Vorlesung geopy versucht, die Geo-Koordinaten aufzulösen.

# geolocator = Nominatim(user_agent="my_app")

# filtered_rows = df_stations[df_stations['latitude'].isnull()]
# result = {}
# # Print the entire row for each entry with NaN latitude
# for index, row in filtered_rows.iterrows():
#     try:
#         address = f'{row["postalCode"]} {row["city"]} {row["state"]} {row["country"]}'
#         result[row['id']] = geolocator.geocode(address)
#     except:
#         pass
# dict={}
# # extract lat/lon
# for index, entry in result.items():
#     if entry:
#         dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }

# geocode_result = pd.DataFrame().from_dict(dict).T
# geocode_result['id'] = geocode_result.index
# geocode_result['id'] = geocode_result['id'].astype(int)

Die Ergebnisse werden in einem Pickel-File gespeichert und daraus wieder geladen.

# output = open(data_folder + 'manual_geocode_results.pkl', 'wb')
# pickle.dump(geocode_result, output)
# output.close()
geocode_result = loadData('manual_geocode_results.pkl')
df_stations
id name metropolis street houseNumber postalCode city state country stationCategory owner organisationalUnit countryCode latitude longitude timeZone
0 1 Aachen Hbf {} Bahnhofstr. 2a 52064 Aachen Nordrhein-Westfalen DE CATEGORY_2 DB S&S RB West DE 50.767800 6.091499 Europe/Berlin
1 1000 Burkhardswalde-Maxen {} Gesundbrunnen 60c 01809 Müglitztal-Burkhardswalde Sachsen DE CATEGORY_7 DB S&S RB Südost DE 50.925146 13.838369 Europe/Berlin
2 1001 Burkhardtsdorf {} Bahnhofstraße NaN 09235 Burkhardtsdorf Sachsen DE CATEGORY_6 DB Regio-Netze Erzgebirgsbahn (EGB) DE NaN NaN Europe/Berlin
3 1002 Bürstadt {} Bahnhofsallee 17 68642 Bürstadt Hessen DE CATEGORY_6 DB S&S RB Mitte DE 49.645769 8.458188 Europe/Berlin
4 1005 Buschow {} Bahnhofstr. 28 14715 Märkisch Luch OT Buschow Brandenburg DE CATEGORY_6 DB S&S RB Ost DE 52.592203 12.628996 Europe/Berlin
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
5685 995 Burgstädt {} Bahnhofstr. 1 09217 Burgstädt Sachsen DE CATEGORY_6 DB S&S RB Südost DE 50.915817 12.812707 Europe/Berlin
5686 996 Burgstall (Murr) {} Bahnhofstr. 1 71576 Burgstetten Baden-Württemberg DE CATEGORY_6 DB S&S RB Südwest DE 48.928647 9.369932 Europe/Berlin
5687 997 Steinfurt-Burgsteinfurt {} Bahnhofsplatz 6 48565 Steinfurt-Burgsteinfurt Nordrhein-Westfalen DE CATEGORY_6 DB S&S RB West DE 52.147384 7.329340 Europe/Berlin
5688 998 Burgthann {} Bahnhofstr. 40 90559 Burgthann Bayern DE CATEGORY_5 DB S&S RB Süd DE 49.342474 11.309307 Europe/Berlin
5689 999 Regensburg-Burgweinting {} Alfons-Goppel-Straße NaN 93055 Regensburg Bayern DE CATEGORY_6 DB S&S RB Süd DE 48.990725 12.146486 Europe/Berlin

5690 rows × 16 columns

Nun liegen die vorhanden Geo-Koordinaten im Data Frame df_stations und die neu ermittelten in geocode_result.
Um nun das Ergebnis aus beiden Tabellen zu bekommen, wird die Funktion combine_first genutzt.

df_stations = df_stations.set_index('id').combine_first(geocode_result.set_index('id')).reset_index()

Anstelle von 282 fehlenden Werten sind es jetzt nur noch 24!
Die restlichen Werte werden aufgrund der geringen Anzahl ignoriert.

df_stations.isna().sum()
id                      0
city                    4
country                 0
countryCode             0
houseNumber           893
latitude               24
longitude              24
metropolis              0
name                    0
organisationalUnit      0
owner                   0
postalCode              7
state                   0
stationCategory        12
street                  8
timeZone                0
dtype: int64
df_stopplaces.isna().sum()
id                        0
name                      0
availableTransports       0
transportAssociations     0
countryCode               0
state                    11
timeZone                  0
latitude                  0
longitude                 0
dtype: int64

Die Haltestellen scheinen eine bessere Datenqualität zu haben. Daher resultieren hieraus keine Probleme, die betrachtet werden müssen.

df_facilities.isna().sum()
id                   0
description         51
operatorname         0
state                0
stateExplanation     0
type                 0
dtype: int64

Einige Beschreibungen sind leer. Diese beschreiben meist, wohin der Aufzug oder die Rolltreppe führen, z.B. “zu Gleis 1”. Aus dieser Information lässt sich nichts ableiten, somit sind die leeren Felder zu vernachlässigen.
Das Gute ist, dass weder die Beschreibung type noch state jemals leer ist.

df_facilities.head(1)
id description operatorname state stateExplanation type
0 1 zu Gleis 1 DB Station&Service ACTIVE available ELEVATOR
df_facilities[df_facilities['description'].isna()]
id description operatorname state stateExplanation type
4 1 None DB Station&Service INACTIVE under construction ELEVATOR
5 1 None DB Station&Service INACTIVE under construction ELEVATOR
6 1 None DB Station&Service INACTIVE under construction ELEVATOR
7 1 None DB Station&Service INACTIVE under construction ELEVATOR
274 1390 None DB Station&Service ACTIVE available ELEVATOR
375 161 None DB Station&Service ACTIVE available ELEVATOR
630 1866 None DB Station&Service ACTIVE available ELEVATOR
648 1866 None DB Station&Service ACTIVE available ESCALATOR
660 1867 None DB Station&Service ACTIVE available ELEVATOR
661 1867 None DB Station&Service UNKNOWN monitoring disrupted ELEVATOR
662 1867 None DB Station&Service ACTIVE available ELEVATOR
676 1877 None DB Station&Service ACTIVE available ELEVATOR
687 1907 None DB Station&Service ACTIVE available ELEVATOR
688 1907 None DB Station&Service ACTIVE available ELEVATOR
794 220 None DB Station&Service INACTIVE under construction ESCALATOR
861 2420 None DB Station&Service ACTIVE available ELEVATOR
862 2420 None DB Station&Service ACTIVE available ELEVATOR
1010 2545 None DB Station&Service ACTIVE available ESCALATOR
1024 2551 None DB Station&Service ACTIVE available ELEVATOR
1042 2618 None DB Station&Service ACTIVE available ELEVATOR
1043 2618 None DB Station&Service ACTIVE available ELEVATOR
1133 2827 None DB Station&Service ACTIVE available ELEVATOR
1462 3631 None DB Station&Service ACTIVE available ESCALATOR
1752 4234 None DB Station&Service ACTIVE available ESCALATOR
1755 4234 None DB Station&Service ACTIVE available ESCALATOR
1756 4234 None DB Station&Service ACTIVE available ESCALATOR
1761 4234 None DB Station&Service ACTIVE available ELEVATOR
1775 4234 None DB Station&Service ACTIVE available ESCALATOR
1776 4234 None DB Station&Service ACTIVE available ESCALATOR
1783 4236 None DB Station&Service UNKNOWN monitoring not available ELEVATOR
1828 4242 None DB Station&Service ACTIVE available ESCALATOR
1848 4258 None DB Station&Service UNKNOWN monitoring disrupted ESCALATOR
2007 4587 None DB Station&Service ACTIVE available ELEVATOR
2026 4593 None DB Station&Service ACTIVE available ELEVATOR
2027 4593 None DB Station&Service ACTIVE available ELEVATOR
2028 4593 None DB Station&Service ACTIVE available ELEVATOR
2035 4593 None DB Station&Service ACTIVE available ELEVATOR
2036 4593 None DB Station&Service ACTIVE available ELEVATOR
2037 4593 None DB Station&Service ACTIVE available ELEVATOR
2041 4593 None DB Station&Service ACTIVE available ELEVATOR
2176 4809 None DB Station&Service ACTIVE available ESCALATOR
2177 4809 None DB Station&Service INACTIVE not available ESCALATOR
2178 4809 None DB Station&Service ACTIVE available ESCALATOR
2179 4809 None DB Station&Service ACTIVE available ESCALATOR
2763 5844 None DB Station&Service ACTIVE available ELEVATOR
2764 5844 None DB Station&Service ACTIVE available ELEVATOR
2840 6071 None DB Station&Service ACTIVE available ESCALATOR
3064 6612 None DB Station&Service ACTIVE available ELEVATOR
3065 6612 None DB Station&Service ACTIVE available ELEVATOR
3408 8097 None DB Station&Service ACTIVE available ESCALATOR
3409 8097 None DB Station&Service ACTIVE available ESCALATOR

Daten zusammen bringen

Jetzt können alle Daten pro Bahnhofsstation zusammengebracht werden.

Dazu werden zunächst alle doppelten Spalten entfernt und dann die Tabellen mithilfe zweier merge zusammengefügt.
Davor wird nochmal stichprobenartig geprüft, ob die IDs auch wirklich zusammenpassen.

Die Facilities können dabei nicht gejoined werden, da ein Bahnhof in der Regel mehrere davon aufweist (1:n Beziehung)

df_stopplaces[df_stopplaces['name'] == 'Ahrensfelde']
id name availableTransports transportAssociations countryCode state timeZone latitude longitude
1539 28 Ahrensfelde [REGIONAL_TRAIN] [VBB] DE BE Europe/Berlin 52.571375 13.565154
df_stations[df_stations['name'] == 'Ahrensfelde']
id city country countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street timeZone
23 28 Berlin DE DE NaN 52.571375 13.565154 {} Ahrensfelde RB Ost DB S&S 12689 Berlin CATEGORY_4 Märkische Allee Europe/Berlin
df_stopplaces.drop(columns=['name', 'state', 'countryCode', 'latitude', 'longitude','timeZone'], inplace=True)
df = pd.merge(df_stations, df_stopplaces, on='id', how='left')
df = pd.merge(left=df, right=df_images, left_on=['id'], right_on=['index'], how='left')
df.drop(columns=['timeZone','index','country'], inplace=True)
df.isna().sum()
id                         0
city                       4
countryCode                0
houseNumber              903
latitude                  24
longitude                 24
metropolis                 0
name                       0
organisationalUnit         0
owner                      0
postalCode                 7
state                      0
stationCategory           12
street                     8
availableTransports       18
transportAssociations     18
image                     64
dtype: int64

Es fehlen nun noch einzelne Werte, aber mit dieser Datengrundlage kann gut gearbeitet werden.

Datenvisualisierung

Anhand der Daten ergeben sich unterschiedliche Fragen:

    1. Sind tatsächlich nur deutsche Stationen vorhanden?
    1. Wie sind die Betreiber owner und Verkehrsverbünde organisationalUnit ausgeprägt?
    1. Wie verteilen sich die Stationen innerhalb von Deutschland (Geo-Koordinaten anschauen)
    1. Wie sind die Transportmittel transportAssociations ausgeprägt?
    1. Wofür stehen die Werte von stationCategory?
df.head(1)
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
0 1 Aachen DE 2a 50.7678 6.091499 {} Aachen Hbf RB West DB S&S 52064 Nordrhein-Westfalen CATEGORY_2 Bahnhofstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] https://api.railway-stations.org/photos/de/1_1...

a) Bahnhöfe der Schweiz

Es gibt im Datenbestand auch einige Bahnhöfe, die in der Schweiz liegen.

Schaffhausen ist ein Gemeinschaftsbahnhof zwischen der Schweizerischen Bundesbahnen und dem deutschen Bundeseisenbahnvermögen. Quelle: Wikipedia

Auffällig ist, dass bei diesen Bahnhöfen keine Bilder und auch keine stationCategory / transportAssociations vorhanden sind. Dies scheint nur im Datenbestand der deutschen Bahnhöfe zu existieren.

df[df['countryCode']!='DE']
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
347 424 Basel CH 200 47.567288 7.607805 {} Basel Bad Bf RB Südwest DB S&S 4016 Schweiz CH NaN Schwarzwaldallee [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [RVL] NaN
2093 2698 Schaffhausen CH 1 47.717003 8.664127 {} Herblingen RB Südwest DB S&S 8207 Schweiz CH NaN Bruderhalde [CITY_TRAIN] [] NaN
3368 4399 Neuhausen CH 18 47.682615 8.612186 {} Neuhausen Bad Bf RB Südwest DB S&S 8212 Schweiz CH NaN Badischen Bahnhofstr. [REGIONAL_TRAIN] [] NaN
3387 4424 Neunkirch CH 3 47.689151 8.495384 {} Neunkirch RB Südwest DB S&S 8225 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [] NaN
4053 5274 Riehen CH 25 47.583157 7.652014 {} Riehen RB Südwest DB S&S 4125 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [RVL] NaN
4233 5530 Schaffhausen CH 29 NaN NaN {} Schaffhausen RB Südwest DB S&S 8200 Schweiz CH NaN Bahnhofstr. NaN NaN NaN
4715 6192 Thayngen CH 31 47.745502 8.704300 {} Thayngen RB Südwest DB S&S 8240 Schweiz CH NaN Bahnhofstr. [CITY_TRAIN] [] NaN
4743 6235 Trasadingen CH 1 47.665238 8.436804 {} Trasadingen RB Südwest DB S&S 8219 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [] NaN
5128 6762 Wilchingen CH 18 47.679448 8.463860 {} Wilchingen-Hallau RB Südwest DB S&S 8217 Schweiz CH NaN Bahnhofstrasse [REGIONAL_TRAIN] [] NaN

b) Betreiber und Verkehrsbünde

def plot_counts(column):
    counts = df[column].value_counts().reset_index()
    counts.columns = [column, 'count']

    fig = px.bar(counts, x=column, y='count', barmode='group', text='count')
    fig.show()

Die meisten Bahnhöfe gehören der DB Station&Service AG. Laut ihrer Webseite, unterhalten sie rund 5.400 Bahnhöfe.

Das können wir bestätigen! The DB S&S hat laut den Daten 5.413 Bahnhöfe. Der Rest, 277, werden von der DB Regio-Netze unterhalten.

plot_counts('owner')
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Betrachtet man die Organisationsbereiche, gibt es viele Stationen in der Mitte, im Süden und im Westen von Deutschland. Der Norden und Osten liegen hingegen auf den letzten Plätzen.

Es gibt auch einige kleinere Organisationseinheiten für spezielle Regionen.

plot_counts('organisationalUnit')
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Tatsächlich ist zu erkennen, dass die führenden Bundesländer Bayern, Baden-Württemberg und Nordrhein-Westfalen (NRW) sind.
Es besteht eine deutliche Lücke zwischen ihnen und dem viertplatzierten Bundesland Hessen. Natürlich muss bei der Anzahl der Bahnhöfe aber auch die Größe der Bundesländer berücksichtigt werden.

Daher wird geprüft, welches Bundesland laut seiner Größe die meisten Bahnhöfe hat. Dazu kann die Größe der Bundesländer abgerufen und ins Verhältnis mit der Anzahl der Stationen gesetzt werden.

c) Bahnhofsdichte in Deutschland

Größe der Bundesländer:

df_states = pd.read_csv(data_folder + 'states_size.csv', sep=';')
df_states['size'] = df_states['size'].astype(float)
df_states
state size
0 Baden-Württemberg 35747.82
1 Bayern 70541.57
2 Berlin 891.12
3 Brandenburg 29654.35
4 Bremen 419.62
5 Hamburg 755.09
6 Hessen 21115.64
7 Mecklenburg-Vorpommern 23295.45
8 Niedersachsen 47709.82
9 Nordrhein-Westfalen 34112.44
10 Rheinland-Pfalz 19858.00
11 Saarland 2571.11
12 Sachsen 18449.93
13 Sachsen-Anhalt 20459.12
14 Schleswig-Holstein 15804.30
15 Thüringen 16202.39
16 Deutschland 357587.77

Anzahl Bahnhöfe pro Bundesland:

df_grp_states = pd.DataFrame(df_stations.groupby(by='state').count()['id'].sort_values(ascending=False))
df_grp_states.rename(columns={'id': 'count'}, inplace=True)
df_grp_states
count
state
Bayern 1025
Baden-Württemberg 720
Nordrhein-Westfalen 711
Hessen 479
Sachsen 478
Rheinland-Pfalz 419
Niedersachsen 357
Brandenburg 310
Sachsen-Anhalt 289
Thüringen 289
Mecklenburg-Vorpommern 180
Schleswig-Holstein 137
Berlin 133
Saarland 77
Hamburg 58
Bremen 16
Schweiz CH 12
df_germany = pd.DataFrame(index=['count'], data={
    'Deutschland':df_grp_states.sum().values[0]
}).T

df_grp_states = pd.concat([df_grp_states, df_germany])
df_grp_states['state'] = df_grp_states.index
df_grp_states = pd.merge(df_states, df_grp_states, how='left', on='state')
df_grp_states
state size count
0 Baden-Württemberg 35747.82 720
1 Bayern 70541.57 1025
2 Berlin 891.12 133
3 Brandenburg 29654.35 310
4 Bremen 419.62 16
5 Hamburg 755.09 58
6 Hessen 21115.64 479
7 Mecklenburg-Vorpommern 23295.45 180
8 Niedersachsen 47709.82 357
9 Nordrhein-Westfalen 34112.44 711
10 Rheinland-Pfalz 19858.00 419
11 Saarland 2571.11 77
12 Sachsen 18449.93 478
13 Sachsen-Anhalt 20459.12 289
14 Schleswig-Holstein 15804.30 137
15 Thüringen 16202.39 289
16 Deutschland 357587.77 5690
  • Gemäß seiner Größe hat Berlin die meisten Haltestellen, wobei jede Station durchschnittlich etwa 6,7 km² abdeckt. Für eine Hauptstadt ergibt dies Sinn.
  • Im Allgemeinen haben alle großen Städte (Hamburg, Bremen) dieses gute Verhältnis.
  • Zuvor waren Bayern und Baden-Württemberg die Regionen mit den meisten Stationen, nun befinden sie sich auf den Plätzen 11 und 8.
  • Das Saarland, das die wenigsten Bahnhöfe hat, weist das beste Verhältnis der Fläche zu Stationen unter den Bundesländern auf.
  • In Niedersachsen muss eine Station im Durchschnitt etwa 133,6 km² abdecken. Wenn man diese Strecke dahingehen auslegt, dass diese mit dem Auto zurückgelegt werden muss, um zur nächstgelegenen Bahnstation zu gelangen, ist das sehr viel.
  • Der Durchschnitt für ganz Deutschland liegt bei 62 km².
df_grp_states['ratio'] = df_grp_states['size']/df_grp_states['count']
df_grp_states.sort_values('ratio', ascending=True).reset_index().drop(columns=['index'])
state size count ratio
0 Berlin 891.12 133 6.700150
1 Hamburg 755.09 58 13.018793
2 Bremen 419.62 16 26.226250
3 Saarland 2571.11 77 33.391039
4 Sachsen 18449.93 478 38.598180
5 Hessen 21115.64 479 44.082756
6 Rheinland-Pfalz 19858.00 419 47.393795
7 Nordrhein-Westfalen 34112.44 711 47.978115
8 Baden-Württemberg 35747.82 720 49.649750
9 Thüringen 16202.39 289 56.063633
10 Deutschland 357587.77 5690 62.844951
11 Bayern 70541.57 1025 68.821044
12 Sachsen-Anhalt 20459.12 289 70.792803
13 Brandenburg 29654.35 310 95.659194
14 Schleswig-Holstein 15804.30 137 115.359854
15 Mecklenburg-Vorpommern 23295.45 180 129.419167
16 Niedersachsen 47709.82 357 133.640952

Um diese Informationen auf einer Karte zu verdeutlichen, fehlen noch die Geo-Koordinaten der Bundesländer. Diese werden wie zuvor abgerufen.

geolocator = Nominatim(user_agent="my_app")

result={}
for entry in df_grp_states['state']:
        result[entry] = geolocator.geocode(entry)
res_dict={}
# extract lat/lon
for index, entry in result.items():
    if entry:
        res_dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }

geocode_result = pd.DataFrame().from_dict(res_dict).T
geocode_result['state'] = geocode_result.index
geocode_result
latitude longitude state
Baden-Württemberg 48.537750 9.041169 Baden-Württemberg
Bayern 48.946756 11.403872 Bayern
Berlin 52.517037 13.388860 Berlin
Brandenburg 52.845549 13.246130 Brandenburg
Bremen 53.075820 8.807165 Bremen
Hamburg 53.550341 10.000654 Hamburg
Hessen 50.608065 9.028465 Hessen
Mecklenburg-Vorpommern 53.773506 12.575547 Mecklenburg-Vorpommern
Niedersachsen 52.839853 9.075962 Niedersachsen
Nordrhein-Westfalen 51.478921 7.554375 Nordrhein-Westfalen
Rheinland-Pfalz 49.953160 7.310646 Rheinland-Pfalz
Saarland 49.384187 6.953737 Saarland
Sachsen 50.929580 13.458505 Sachsen
Sachsen-Anhalt 52.008907 11.700334 Sachsen-Anhalt
Schleswig-Holstein 54.185400 9.822009 Schleswig-Holstein
Thüringen 50.901472 11.037784 Thüringen
Deutschland 51.163818 10.447831 Deutschland
df_grp_states_geo = pd.merge(df_grp_states, geocode_result, how='left', on='state')
df_grp_states_geo.drop(16, inplace=True) # drop germany

In diesem Fall wird plotly genutzt, um eine Karte anzuzeigen.

Die Größe der Punkte zeigt die Anzahl der Haltestationen an.
Die Farbe gibt das Verhältnis zur Fläche an: Grün steht für eine hohe Dichte an Stationen, Rot für eine niedrige.

Hier wird nochmal deutlich, dass die Stadtstaaten eine hohe Dichte an Haltestationen aufweisen, allerdings absolut gesehen wenige Stationen haben (kleiner Kreis).
Die großen Bundesländer sind eher im mittleren Farbschema, wohingegen die Bundesländer im Norden rot gefärbt und damit das Schlusslicht bilden.

fig = px.scatter_geo(df_grp_states_geo,
                     lat='latitude', 
                     lon='longitude',
                     hover_name='state',     # Data to display when hovering over each data point
                     size='count',     # Size of the markers
                     color='ratio',    # Color of the markers
                     color_continuous_scale=['green','orange','red'],
                     projection='mercator',
                     scope='europe',
                     width=650,
                     height=800)  # Map projection

fig.update_geos(center=dict(lon=10, lat=51), projection_scale=10)
fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

d) Verkehrsmittel und Verkehrsverbünde

Da die Verkehrsmittel und Verkehrsverbünde in geschachtelten Listen vorliegen, müssen diese zunächst geebnet werden, um die absolute Anzahl herauszufinden.

transports = []
for entry in df['transportAssociations']:
    try:
        for e in entry:
            transports.append(e)
    except:
        pass

transportAssociations = pd.Series(transports).value_counts()
transports = []
for entry in df['availableTransports']:
    try:
        for e in entry:
            transports.append(e)
    except:
        pass

availableTransports = pd.Series(transports).value_counts()
def plotBar(data, title, xlabel, ylabel, showlegend):
    fig = px.bar(data, title=title)
    fig.update_xaxes(title_text=xlabel)
    fig.update_yaxes(title_text=ylabel)
    fig.update_traces(showlegend=showlegend)
    return fig
fig = plotBar(transportAssociations, 'Available Transport Associations', 'Transport Associations', 'Count', False)
fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json
transportAssociations.count()
49

Spitzenreiter ist auch hier wieder Berlin, gefolgt von dem Rhein-Main-Verkehrsverbund (RMV).

Der RMV operiert in Hessen und ist der Nachfolger des Frankfurter Verkehrsverbundes (FVV) (Quelle: Wikipedia) Auf dem nachfolgenden Bild sieht man das Verkehrsgebiet in Hessen.

Die “NASA” ist der Nahverkehrsservice der Sachsen-Anhalt GmbH und hat lustigerweise auch die Webseite https://www.nasa.de/.

Auf dem Plan ist zu sehen, dass hauptsächlich die Städte Leipzig, Halle, Dessau und Magdeburg verbunden werden. Der Verkehrsverbund “NASA” besteht wiederum aus anderen, die ebenfalls hier auftauchen, wie der MDV. Es gibt Übergänge zu anderen Verkehrsverbünden wie VMT, VRB und VBB.

Die in unserer Region bekannteren Verkehrsverbünde VVS und NALDO rangieren auf den mittleren Plätzen. Insgesamt gibt es 49 unterschiedliche Verkehrsverbünde.

fig = plotBar(availableTransports, 'Available Transports', 'Transport Type', 'Count', False)
fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Mit großem Abstand halten an den meisten Haltestationen die REGIONAL_TRAIN. Leider ist nicht dokumentiert, was unter “Regional Train” zu verstehen ist.

Nach manueller Untersuchung der Daten, sind damit sowohl Regionalbahnen (RB) wie auch S-Bahnen gemeint. Allerdings werden die S-Bahn Stationen in der Stadt mit CITY_TRAIN markiert. Das ist vorliegend also nicht ganz eindeutig. CITY_TRAIN sind die Stadtbahnen/Tram/U-Bahn sowie S-Bahnen in der Stadt.

Neben Zügen werden hier auch Busse (BUS) erfasst.

Im Vergleich dazu kommen die Schnellzüge INTERCITY_TRAIN (IC), HIGH_SPEED_TRAIN (ICE) und INTER_REGIONAL_TRAIN (IRE) fast schon selten vor. Das macht aber natürlich Sinn, weil diese nur an ausgewählten Bahnhöfen halten.

Hier gibt es eine Übersicht der Bahn über die Nah- und Fernverkehrszüge.

e) Bahnhofskategorien

Laut Theorie klassifiziert die Preisklasse (bis 2018 Bahnhofskategorie) anhand verschiedener Faktoren die Bedeutung eines Bahnhofs für den Personenverkehr sowie den Service, der dort geboten wird. (Quelle: Wikipedia)

Demnach wird die Preisklasse aufgrund folgender Kriterien ermittelt:

Daraus lässt sich ein Wert errechnen, der die Preisklasse angibt:

Beispiel für einen Bahnhof mit 4 Bahnsteigkanten von max. 250 Meter Länge, 20.000 Reisenden und 280 Zughalten am Tag, ohne anwesenden Mitarbeiter, jedoch mit technischer Stufenfreiheit:

Der Bahnhof in dem Beispiel hätte also Klasse 3.

Laut Wikipedia ergibt sich für Deutschland folgende Einteilung. Es wird nun geprüft, ob sich dies mit den vorhandenen Daten deckt.

Kategorie Anzahl Beispiel
1 21 Berlin, Hamburg
2 87 Mainz, Trier
3 256 Hof, Bitterfel
4 628 Meiningen, Bingen
5 992 Köln-Holweide, Hohen Neuendorf
6 2505 Ilmenau, Glöwen
7 916 Zwotental, Göhrde

Aufgrund des merge kommen nun ein paar Bahnhöfe doppelt vor, in denen es zwei Haltestationen gibt. Meist sind das größere Bahnhöfe, in denen die Schnellzüge getrennt vom Regionalverkehr abfahren, wie beispielsweise in Berlin.
Um das zu korrigieren, wird jeweils nur der letzte Eintrag bei diesen doppelten Einträgen verwendet.

df[df['id'] == 1071]
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
854 1071 Berlin DE 1 52.525592 13.369545 {} Berlin Hauptbahnhof RB Ost DB S&S 10557 Berlin CATEGORY_1 Europaplatz [CITY_TRAIN] [VBB] https://api.railway-stations.org/photos/de/107...
855 1071 Berlin DE 1 52.525592 13.369545 {} Berlin Hauptbahnhof RB Ost DB S&S 10557 Berlin CATEGORY_1 Europaplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VBB] https://api.railway-stations.org/photos/de/107...
856 1071 Berlin DE 1 52.525592 13.369545 {} Berlin Hauptbahnhof RB Ost DB S&S 10557 Berlin CATEGORY_1 Europaplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VBB] https://api.railway-stations.org/photos/de/107...
unique_df = df.drop_duplicates(subset='id', keep='last')

Betrachtet man nun die Daten von Deutschland, ergibt sich folgendes Bild:

unique_df.groupby(by='stationCategory')['id'].count()
stationCategory
CATEGORY_1      23
CATEGORY_2      84
CATEGORY_3     275
CATEGORY_4     641
CATEGORY_5     977
CATEGORY_6    2774
CATEGORY_7     904
Name: id, dtype: int64

Im Ergebnis decken sich die Daten mit der Einteilung aus Wikipedia.

In Berlin gibt es gleich mehrere Bahnhöfe mit Preisklasse / stationCategory 1. In Hamburg ist es der Hauptbahnhof und Altona. Stuttgart Hbf gehört ebenfalls zur Kategorie 1.

unique_df[unique_df['stationCategory'] == 'CATEGORY_1']
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
183 220 Augsburg DE 1 48.365441 10.885570 {} Augsburg Hbf RB Süd DB S&S 86150 Bayern CATEGORY_1 Viktoriastr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AVV] https://api.railway-stations.org/photos/de/220...
421 528 Berlin DE 1-3 52.548963 13.388513 {} Berlin Gesundbrunnen RB Ost DB S&S 13357 Berlin CATEGORY_1 Badstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VBB] https://api.railway-stations.org/photos/de/528...
424 530 Berlin DE 3 52.510488 13.434681 {} Berlin Ostbahnhof RB Ost DB S&S 10243 Berlin CATEGORY_1 Koppenstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VBB] https://api.railway-stations.org/photos/de/530...
856 1071 Berlin DE 1 52.525592 13.369545 {} Berlin Hauptbahnhof RB Ost DB S&S 10557 Berlin CATEGORY_1 Europaplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VBB] https://api.railway-stations.org/photos/de/107...
1002 1289 Dortmund DE 15 51.517896 7.459290 {} Dortmund Hbf RB West DB S&S 44137 Nordrhein-Westfalen CATEGORY_1 Königswall [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRR, WT] https://api.railway-stations.org/photos/de/128...
1052 1343 Dresden DE 4 51.040563 13.732035 {} Dresden Hbf RB Südost DB S&S 01069 Sachsen CATEGORY_1 Wiener Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VVO] https://api.railway-stations.org/photos/de/134...
1075 1374 Duisburg DE 1 51.429785 6.775903 {} Duisburg Hbf RB West DB S&S 47051 Nordrhein-Westfalen CATEGORY_1 Portsmouthplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRR] https://api.railway-stations.org/photos/de/137...
1097 1401 Düsseldorf DE 14 51.219962 6.794319 {} Düsseldorf Hbf RB West DB S&S 40210 Nordrhein-Westfalen CATEGORY_1 Konrad-Adenauer-Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRS, VRR] https://api.railway-stations.org/photos/de/140...
1329 1690 Essen DE 5 51.451355 7.014793 {} Essen Hbf RB West DB S&S 45127 Nordrhein-Westfalen CATEGORY_1 Am Hauptbahnhof [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRR] https://api.railway-stations.org/photos/de/169...
1476 1866 Frankfurt am Main DE NaN 50.107145 8.663789 {} Frankfurt (Main) Hbf RB Mitte DB S&S 60329 Hessen CATEGORY_1 Im Hauptbahnhof [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [RMV] https://api.railway-stations.org/photos/de/186...
1947 2514 Hamburg DE 16 53.552736 10.006909 {} Hamburg Hbf RB Nord DB S&S 20099 Hamburg CATEGORY_1 Hachmannplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [IAM, SH, HVV] https://api.railway-stations.org/photos/de/251...
1951 2517 Hamburg DE 17 53.552695 9.935175 {} Hamburg-Altona RB Nord DB S&S 22765 Hamburg CATEGORY_1 Scheel-Plessen-Str. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [IAM, SH, HVV] https://api.railway-stations.org/photos/de/251...
1977 2545 Hannover DE 1 52.376761 9.741021 {} Hannover Hbf RB Nord DB S&S 30159 Niedersachsen CATEGORY_1 Ernst-August-Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [IAM, GVH] https://api.railway-stations.org/photos/de/254...
2405 3107 Karlsruhe DE 1a 48.993515 8.402181 {} Karlsruhe Hbf RB Südwest DB S&S 76137 Baden-Württemberg CATEGORY_1 Bahnhofplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [KVV] https://api.railway-stations.org/photos/de/310...
2554 3320 Köln DE 11 50.943030 6.958729 {} Köln Hbf RB West DB S&S 50667 Nordrhein-Westfalen CATEGORY_1 Trankgasse [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRS] https://api.railway-stations.org/photos/de/332...
2564 3329 Köln DE 7 50.940874 6.975001 {} Köln Messe/Deutz RB West DB S&S 50679 Nordrhein-Westfalen CATEGORY_1 Ottoplatz [REGIONAL_TRAIN, BUS, HIGH_SPEED_TRAIN, CITY_T... [VRS] https://api.railway-stations.org/photos/de/332...
2784 3631 Leipzig DE 5 51.345471 12.382064 {} Leipzig Hbf RB Südost DB S&S 04109 Sachsen CATEGORY_1 Willy-Brandt-Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [NASA, MDV] https://api.railway-stations.org/photos/de/363...
3003 3925 Mannheim DE 17 49.479354 8.468921 {} Mannheim Hbf RB Südwest DB S&S 68161 Baden-Württemberg CATEGORY_1 Willy-Brandt-Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VRN] https://api.railway-stations.org/photos/de/392...
3236 4234 München DE 10a 48.140232 11.558335 {} München Hbf RB Süd DB S&S 80335 Bayern CATEGORY_1 Bayerstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [MVV] https://api.railway-stations.org/photos/de/423...
3243 4241 München DE 11 48.127440 11.604971 {} München Ost RB Süd DB S&S 81667 Bayern CATEGORY_1 Orleansplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [MVV] https://api.railway-stations.org/photos/de/424...
3531 4593 Nürnberg DE 9 49.445616 11.082989 {} Nürnberg Hbf RB Süd DB S&S 90443 Bayern CATEGORY_1 Bahnhofsplatz [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [VGN] https://api.railway-stations.org/photos/de/459...
3743 4859 Berlin DE 1 52.475047 13.365319 {} Berlin Südkreuz RB Ost DB S&S 12101 Berlin CATEGORY_1 General-Pape-Straße [] [VBB] https://api.railway-stations.org/photos/de/485...
4622 6071 Stuttgart DE 2 48.784084 9.181635 {} Stuttgart Hbf RB Südwest DB S&S 70173 Baden-Württemberg CATEGORY_1 Arnulf-Klett-Platz [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [VVS] https://api.railway-stations.org/photos/de/607...

Betrachtet man die Bundesländer nach Kategorie, zeigt sich, dass einige Bundesländer keinen einzigen Bahnhof mit Preisklasse 1 besitzen.

df_state_categories = pd.DataFrame(unique_df.groupby(by=['state', 'stationCategory'])['id'].count())
df_state_categories = df_state_categories.reset_index().pivot(index='state', columns='stationCategory', values='id')
df_state_categories
stationCategory CATEGORY_1 CATEGORY_2 CATEGORY_3 CATEGORY_4 CATEGORY_5 CATEGORY_6 CATEGORY_7
state
Baden-Württemberg 3.0 10.0 51.0 86.0 133.0 328.0 109.0
Bayern 4.0 9.0 48.0 100.0 174.0 532.0 158.0
Berlin 4.0 7.0 12.0 72.0 34.0 4.0 NaN
Brandenburg NaN 4.0 13.0 22.0 44.0 174.0 53.0
Bremen NaN 1.0 1.0 3.0 8.0 3.0 NaN
Hamburg 2.0 2.0 8.0 35.0 9.0 2.0 NaN
Hessen 1.0 8.0 24.0 62.0 109.0 248.0 27.0
Mecklenburg-Vorpommern NaN 1.0 7.0 12.0 14.0 90.0 56.0
Niedersachsen 1.0 8.0 15.0 51.0 68.0 165.0 49.0
Nordrhein-Westfalen 6.0 16.0 42.0 101.0 166.0 307.0 73.0
Rheinland-Pfalz NaN 7.0 12.0 37.0 67.0 222.0 74.0
Saarland NaN 1.0 6.0 3.0 14.0 47.0 6.0
Sachsen 2.0 2.0 7.0 26.0 68.0 291.0 82.0
Sachsen-Anhalt NaN 2.0 9.0 11.0 26.0 162.0 79.0
Schleswig-Holstein NaN 4.0 10.0 10.0 28.0 49.0 36.0
Thüringen NaN 2.0 10.0 10.0 15.0 150.0 102.0

Die größten Unterschiede gibt es in Preisklasse 6, hier liegt die Anzahl der Bahnhöfe je Bundesland weit auseinander. Bayern und Baden-Württemberg, welche über die meisten Bahnhöfe verfügen, sind Ausreißer.

In den restlichen Preisklassen ist das nicht so ausgeprägt, besonders NRW (gelb) ist in den oberen Kategorien zusammen mit Bayern und Baden-Württemberg häufig auf den besten Plätzen mit dabei.

fig = px.violin(df_state_categories, y=df_state_categories.columns,color=df_state_categories.index)
fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

In diesem Bild sieht man nochmal gut, dass die meisten Stationen in Preisklasse 6 sind. Bei den Stadtstaaten sind die Werte näher beieinander, dort ist die Anzahl der Stationen insgesamt natürlich geringer.

fig = px.scatter(df_state_categories, y=df_state_categories.columns)
fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Rolltreppen und Aufzüge

df_facilities.head(1)
id description operatorname state stateExplanation type
0 1 zu Gleis 1 DB Station&Service ACTIVE available ELEVATOR

Zunächst werden die Daten in eine Form gebracht, die besser zu visualisieren ist.

Von Interesse ist, welche Art (type) von facilities sich in welchem Zustand (state) befinden.
Dazu wird anhand dieser beiden Werte gruppiert und die summierten Werte wieder in eine flache Struktur geformt.

Man könnte auch noch auf den operatorname eingehen, allerdings werden die allermeisten Einrichtungen wieder von der DB Station&Service betrieben.

df_facilities_grouped = df_facilities.groupby(['type', 'state']).count()
df_facilities_grouped = df_facilities_grouped.unstack()['id']
df_facilities_grouped
state ACTIVE INACTIVE UNKNOWN
type
ELEVATOR 2392 126 51
ESCALATOR 820 140 21
df_facilities_grouped['Ratio'] = (df_facilities_grouped['INACTIVE']) / df_facilities_grouped['ACTIVE']
df_facilities_grouped
state ACTIVE INACTIVE UNKNOWN Ratio
type
ELEVATOR 2392 126 51 0.052676
ESCALATOR 820 140 21 0.170732

Es gibt ein paar Daten, bei denen der Zustand unbekannt ist.

Darüber hinaus sind absolut und relativ gesehen aktuell mehr Rolltreppen als Aufzüge kaputt.
Relativ sind es mit aktuell über 15% (nach Update eine Woche später: 17%) nicht funktionierende Rolltreppen sehr viele.

fig = go.Figure()

for state in df_facilities_grouped.columns[:3]:
    fig.add_trace(go.Bar(
        x=df_facilities_grouped.index,
        y=df_facilities_grouped[state],
        name=state,
    ))

fig.update_layout(title='Zustand der Einrichtungen an Bahnhöfen',
                  xaxis_title='Typ',
                  yaxis_title='Anzahl',
                  barmode='group')

fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Dashboard für Haltestationen

Wir können uns eine Karte anzeigen lassen, die alle Haltestationen mit zusätzliche Informationen anzeigt.

Ganz Deutschland anzuzeigen führt allerding zu Performanceproblemen, daher werden zunächst alle Marker ausgeblendet, welche über den Filter je Bundesland wieder hinzugeschaltet werden können.

df.dropna(subset = ['latitude'], inplace=True)

Dazu wird zuerst pro Bundesland eine FeatureGroup erstellt, die initial ausgeblendet ist.

state_dict = {}
for i in df.index:
    state_dict.setdefault(df['state'][i], folium.FeatureGroup(name=df['state'][i], show=False, autoZIndex=False))
state_dict
{'Nordrhein-Westfalen': <folium.map.FeatureGroup at 0x2303a3b6ce0>,
 'Baden-Württemberg': <folium.map.FeatureGroup at 0x2303a3b6c80>,
 'Bayern': <folium.map.FeatureGroup at 0x2303a3b62c0>,
 'Niedersachsen': <folium.map.FeatureGroup at 0x230389aba00>,
 'Sachsen': <folium.map.FeatureGroup at 0x230389ab940>,
 'Schleswig-Holstein': <folium.map.FeatureGroup at 0x23039d75f30>,
 'Berlin': <folium.map.FeatureGroup at 0x23037be0c10>,
 'Brandenburg': <folium.map.FeatureGroup at 0x23039ab26e0>,
 'Rheinland-Pfalz': <folium.map.FeatureGroup at 0x23039ab2890>,
 'Hessen': <folium.map.FeatureGroup at 0x23039ab1f60>,
 'Hamburg': <folium.map.FeatureGroup at 0x2303a509570>,
 'Mecklenburg-Vorpommern': <folium.map.FeatureGroup at 0x2303a50a050>,
 'Thüringen': <folium.map.FeatureGroup at 0x2303a509720>,
 'Sachsen-Anhalt': <folium.map.FeatureGroup at 0x2303a509ab0>,
 'Saarland': <folium.map.FeatureGroup at 0x2303a5095a0>,
 'Schweiz CH': <folium.map.FeatureGroup at 0x2303a5091b0>,
 'Bremen': <folium.map.FeatureGroup at 0x2303a508160>}

Per HTML kann ein Popup definiert werden, welches erscheint, sobald man auf den Pin klickt. Hierbei werden das Bild und die zugehörigen Verkehrsverbünde sowie Zugtypen angezeigt.

Auf dieser Karte kann man sehr schön erkennen, wo die Bahnlinien verlaufen und welche Regionen aktuell nicht an die Bahn angeschlossen sind.

Beim Klicken durch die Bilder fällt auch auf, dass die Bahnhöfe häufig in älteren Gebäuden untergebracht sind, von den allerdings viele renoviert wurden.

Die roten Marker identifizieren Bahnhöfe, in denen ICEs halten. In den gelben Markern halten “nur noch” die IREs.

def GetIcon(availableTransports):
    try:
        if ('HIGH_SPEED_TRAIN' in availableTransports):
            return folium.Icon(color='red', icon='map-marker')
        elif ('INTERCITY_TRAIN' in availableTransports):
            return folium.Icon(color='orange', icon='map-marker')
    except:
        return folium.Icon(color='blue', icon='map-marker')
map_df = df

m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.

for i in map_df.index:
    html=f"""
    <img src="{map_df['image'][i]}" width="500px">
    <br/>
    <b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
    <p>Transports: {map_df['availableTransports'][i]}</p>
    <p>Associations: {map_df['transportAssociations'][i]}</p>
    """

    parsedHtml = folium.Html(html, script=True)
    popup = folium.Popup(parsedHtml, max_width=2650)

    # this is probably done too often, but folium is smart enough
    feature_group = state_dict[map_df['state'][i]]
    m.add_child(feature_group)

    folium.Marker(
        location=[ map_df['latitude'][i], map_df['longitude'][i] ], 
        icon=GetIcon(map_df['availableTransports'][i]),
        radius=8,
        tooltip=map_df['name'][i],
        popup=popup
    ).add_to(feature_group)
    
folium.LayerControl(collapsed=False).add_to(m)
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Bahnhofsnahe Dienstleistungen

Mögliche Werte von Type aus der Dokumentation:

  • INFORMATION_COUNTER [Informationsstand für Belange im Bahnhof (kein Fahrkartenverkauf)]
  • TRAVEL_CENTER [Reisezentrum]
  • VIDEO_TRAVEL_CENTER [Video Reisezentrum]
  • TRIPLE_S_CENTER [3S Zentrale für Service, Sicherheit & Sauberkeit]
  • TRAVEL_LOUNGE [Lounge (DB Lounge z.B.)]
  • LOST_PROPERTY_OFFICE [Fundstelle]
  • RAILWAY_MISSION [Bahnhofsmission]
  • HANDICAPPED_TRAVELLER_SERVICE [Service für mobilitätseingeschränkte Reisende]
  • LOCKER [Schließfächer]
  • WIFI [WLan]
  • CAR_PARKING [Autoparkplatz, ggf. kostenpflichtig]
  • BICYCLE_PARKING [Fahrradparkplätze, ggf. kostenpflichtig]
  • PUBLIC_RESTROOM [Öffentliches WC, ggf. kostenpflichtig]
  • TRAVEL_NECESSITIES [Geschäft für den Reisendenbedarf]
  • CAR_RENTAL [Car-Sharer oder Mietwagen]
  • BICYCLE_RENTAL [Mieträder]
  • TAXI_RANK [Taxi Stand]
  • MOBILE_TRAVEL_SERVICE [Mobiler Service]
  • RAD_PLUS (Rad+ Gebiet)
df_local_services = loadData('local_services.pkl')
df_local_services.head()
id name description openingHours latitude longitude type
0 1 None None Mo-Su 06:15-22:30;PH 06:15-22:30 MOBILE_TRAVEL_SERVICE
1 1 Duisburg Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
2 1 None None None RAILWAY_MISSION
3 1 None Ja, um Voranmeldung unter 030 65 21 28 88 (Ort... None HANDICAPPED_TRAVELLER_SERVICE
4 1 None None None LOCKER

Man erkennt, dass die TRIPLE_S_CENTER (Service, Sicherheit & Sauberkeit), TRAVEL_CENTER, TRAVEL_LOUNGE und VIDEO_TRAVEL_CENTER alle einen Namen, Öffnungszeiten und Geo-Koordinaten haben.

Die Services RAD_PLUS und TRAVEL_LOUNGE haben einen Namen, MOBILE_TRAVEL_SERVICE, INFORMATION_COUNTER und LOST_PROPERTY_OFFICE dafür Öffnungszeiten.

df_local_services.groupby(by='type').count().sort_values(by='id', ascending=False)
id name description openingHours latitude longitude
type
TRIPLE_S_CENTER 4103 4103 0 4103 4103 4103
CAR_PARKING 3136 0 0 0 3136 3136
BICYCLE_PARKING 3081 0 0 0 3081 3081
TAXI_RANK 1100 0 0 0 1100 1100
PUBLIC_RESTROOM 562 0 0 0 562 562
TRAVEL_NECESSITIES 508 0 0 0 508 508
HANDICAPPED_TRAVELLER_SERVICE 318 0 233 0 318 318
TRAVEL_CENTER 260 260 260 260 260 260
RAD_PLUS 260 260 0 0 260 260
LOCKER 168 0 0 0 168 168
MOBILE_TRAVEL_SERVICE 127 0 0 127 127 127
WIFI 119 0 0 0 119 119
RAILWAY_MISSION 89 0 0 0 89 89
VIDEO_TRAVEL_CENTER 87 87 87 87 87 87
INFORMATION_COUNTER 76 0 0 76 76 76
LOST_PROPERTY_OFFICE 70 0 70 70 70 70
CAR_RENTAL 67 0 0 0 67 67
TRAVEL_LOUNGE 14 14 0 14 14 14
def getDataByType(type):
    filtered_ids = df_local_services[df_local_services['type'] == type]['id']

    count = filtered_ids.count()
    unique_count = filtered_ids.nunique()

    print(type,"Count:", count)
    print(type,"unique Count:", unique_count)

    return df_local_services[df_local_services['type'] == type]
df_triples_center = getDataByType('TRIPLE_S_CENTER')
TRIPLE_S_CENTER Count: 4103
TRIPLE_S_CENTER unique Count: 4103

Wenn man sich die Daten anschaut, tauchen dort viele IDs doppelt auf und es scheint, als wären die Daten schwierig zu interpretieren.
Gruppiert man die Daten allerdings anhand des Typs, sind die IDs eindeutig und können somit gut analysiert werden.

df_triples_center.head(3)
id name description openingHours latitude longitude type
1 1 Duisburg Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
13 1000 Dresden None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
16 1002 Frankfurt (Main) Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER

Die Öffnungszeiten sind in einem bestimmten Format angegeben, daher müssen diese erst geparst werden.
Zuvor wird geprüft, ob es überhaupt Abweichungen gibt.

df_triples_center['openingHours'].unique()
array(['Mo-Su 00:00-24:00;PH 00:00-24:00'], dtype=object)

Scheinbar haben alle TRIPLE_S_CENTER durchgehend geöffnet.

df_travel_center = getDataByType('TRAVEL_CENTER')
TRAVEL_CENTER Count: 260
TRAVEL_CENTER unique Count: 257
df_travel_center.head(1)
id name description openingHours latitude longitude type
11 1 DB Reisezentrum Aachen Hbf Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00 50.768944 6.0902 TRAVEL_CENTER

Bei den Reisezentren sehen die Öffnungszeiten schon interessanter aus.

df_travel_center['openingHours'].unique()
array(['Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00',
       'Mo-We 08:00-12:30,13:00-17:00;Th-Fr 08:00-12:30,13:00-18:00;Sa 08:00-13:30',
       'Mo-Fr 08:00-17:00;Sa 08:00-13:00;Su 10:00-15:00',
       'Mo 06:00-11:00,12:00-16:00;Tu-We,Fr 08:00-13:00,14:00-16:00;Th 09:00-13:00,14:00-19:00;Sa 08:00-12:00',
       'Mo-Fr 06:30-12:00,13:00-18:30;Sa 08:00-13:00',
       'Mo-Su 07:00-21:00',
       'Mo-Fr 07:30-18:30;Sa-Su 09:00-13:00,13:30-17:00',
       'Mo-Fr 09:00-12:00,13:00-17:25',
       'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:30-13:30',
       'Mo-Fr 08:00-18:00', 'Mo-Fr 07:00-19:00;Sa 09:00-14:00',
       'Mo-Fr 07:00-11:30,12:00-14:30;Sa 08:30-13:30',
       'Mo-Fr 06:30-09:00,09:30-17:30',
       'Mo 06:30-18:30;Tu-Fr 07:30-18:30;Sa 06:30-11:30,12:00-14:30;Su 10:30-14:30,15:00-18:30',
       'Mo-Fr 06:30-12:00,13:00-18:30', 'Mo-Fr 09:00-12:30,13:30-17:00',
       'Mo 06:00-10:30,11:30-16:00;Tu 09:30-13:30,14:30-19:00;We-Fr 08:30-12:30,13:30-16:00;Sa 08:00-12:00',
       'Mo-Fr 09:00-12:30,14:00-17:45',
       'Mo 06:00-11:30,12:20-16:30;Tu-Fr 07:00-11:30,12:15-16:35',
       'Mo-Fr 07:30-19:00;Sa-Su 09:00-18:00',
       'Mo,Fr 06:15-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Tu-Th 06:15-12:00,12:45-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Sa 07:10-12:00 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. "',
       'Mo-Fr 06:45-20:00;Sa-Su 07:00-20:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-16:00', 'Mo-Fr 06:30-11:45',
       'Mo-Fr 07:00-18:00;Sa 08:00-12:00,12:30-14:30',
       'Mo-Fr 07:00-21:00;Sa-Su 09:00-21:00',
       'Mo 06:00-15:00,15:45-20:00;Tu-Fr 07:00-12:45,13:45-17:00;Sa-Su 09:30-15:00',
       'Mo-We,Fr 08:30-13:00,13:45-18:00', 'Mo-Fr 06:30-17:30',
       'Mo-Fr 05:45-18:45;Sa 08:00-13:00',
       'Mo-Fr 07:30-12:30,13:30-17:30;Sa 07:30-12:30',
       'Mo-Fr 08:00-13:00,14:00-17:45',
       'Mo-Fr 07:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 08:30-18:30;Sa-Su 09:30-16:30',
       'Mo 07:00-12:15,13:00-16:30;Tu-We,Fr 09:00-12:15,13:00-16:30;Th 09:00-12:15,13:00-18:00',
       'Mo-Fr 07:30-19:00;Sa 08:15-15:00;Su 09:15-15:00',
       'Mo-Fr 07:30-18:30;Sa 09:00-14:00',
       'Mo 06:00-17:30;Tu-We,Fr 09:00-17:30;Th 09:00-20:00;Sa 08:00-16:30;Su 09:00-12:30,13:00-16:30',
       'Mo-Fr 06:00-20:00;Sa 07:30-12:15,12:45-15:30',
       'Mo-Fr 07:30-21:00;Sa-Su 08:30-18:30',
       'Mo 06:30-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";Tu,Fr 07:00-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";We-Th 07:00-11:00,11:45-16:15 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!"',
       'Mo-Fr 07:00-19:00;Sa 07:30-11:30,12:00-16:00;Su 09:30-15:00',
       'Mo-Fr 06:00-20:30;Sa 07:00-14:30',
       'Mo-Fr 07:30-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:30-15:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 11:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 07:00-11:30,13:30-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:00-20:00;Sa 08:30-19:00;Su 08:30-20:00',
       'Mo-Fr 08:30-13:00,14:00-18:00;Sa 09:00-13:45',
       'Mo 07:15-12:00,13:00-17:45;Tu-Fr 08:00-12:00,13:00-17:45;Sa 08:00-13:30',
       'Mo-Fr 07:00-18:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum.";Sa 08:00-13:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum."',
       'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:00-13:30',
       'Mo-Fr 07:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 08:00-18:30;Sa 08:15-16:30;Su 12:00-17:00',
       'Mo-Fr 07:00-19:00;Sa 09:00-14:30', 'Mo-Fr 07:15-17:15',
       'Mo-Fr 08:30-17:30;Sa 08:30-13:30',
       'Mo-Fr 07:45-12:00,13:30-18:00;Sa 08:10-13:45',
       'Mo-Fr 09:00-18:00',
       'Mo,Fr 07:00-18:00;Tu-Th 08:00-12:30,13:15-18:00;Sa 08:00-13:15;Su 10:00-15:15',
       'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:00-12:30,13:30-17:00;Sa 07:00-12:30,13:00-15:00;Su 09:30-15:00',
       'Mo-Fr 07:00-18:30;Sa 09:00-14:00',
       'Mo-Fr 08:00-19:00;Sa 09:00-18:00;Su 10:00-16:00',
       'Mo-Fr 07:45-12:30,13:30-17:45;Sa 09:00-14:00',
       'Mo,Fr 06:00-18:00;Tu-Th 06:45-12:00,12:45-17:00;Su 09:00-12:30,13:00-17:00',
       'Mo-Fr 07:00-19:00;Sa 09:00-17:00;Su 10:00-17:00',
       'Mo-Fr 07:00-12:15,13:00-17:00;Sa 09:00-12:30,13:00-17:00;Su 08:00-12:30,13:00-15:00',
       'Mo-Fr 08:00-12:30,13:30-17:00;Sa 10:00-15:30',
       'Mo-Fr 07:15-12:15,13:00-17:30;Sa-Su 08:10-12:15,12:45-15:50',
       'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch persönlich im DB Videoreisezentrum"',
       'Mo-Fr 06:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Sa 07:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Su 08:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr"',
       'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:15', 'Mo-Fr 07:30-18:30',
       'Mo-Fr 07:40-18:30;Sa 08:00-13:30;Su 09:00-14:30',
       'Mo-Fr 06:30-19:00;Sa 08:00-17:30;Su 10:00-18:00',
       'Mo-Fr 07:00-19:00;Sa 09:30-14:30',
       'Mo-Sa 07:00-20:00;Su 08:00-20:00',
       'Mo-Fr 09:00-12:00,13:00-17:15',
       'Mo-Fr 07:45-13:00,13:45-17:30;Sa 07:45-12:45',
       'Mo-Fr 07:00-20:00;Sa-Su 09:00-19:00',
       'Mo-Fr 07:45-12:30,13:30-18:00;Sa 08:45-14:20',
       'Mo-Fr 07:30-18:30;Sa 08:30-15:00;Su 10:00-16:00',
       'Mo 07:00-12:30,13:00-17:00;Tu,Fr 08:00-12:30,13:00-17:00;We-Th 08:00-12:30,13:00-18:00;Sa 07:30-12:30,13:00-15:00;Su 10:00-15:00',
       'Mo,Th 06:30-18:00;Tu-We,Fr 06:30-12:00,13:00-17:00;Sa 06:30-12:00,13:00-15:00',
       'Mo-Fr 06:50-18:00;Sa 08:30-13:00',
       'Mo,Fr 06:45-18:00;Tu-Th 07:20-12:00,13:00-17:30',
       'Mo-Th 08:00-12:00,12:45-16:30;Fr 08:00-13:30',
       'Mo-We,Fr 07:30-11:30,12:30-17:30;Th 06:00-15:00,15:30-20:00;Sa 08:30-14:00;Su 10:00-15:00',
       'Mo-Fr 09:00-13:00,14:00-17:30;Sa 09:00-13:00',
       'Mo-Fr 07:45-12:00,12:45-17:30;Sa 08:00-13:00',
       'Mo 06:00-20:00;Tu-Fr 07:00-20:00;Sa-Su 08:00-13:00,14:00-16:30',
       'Mo-We,Fr 07:30-12:30,13:30-17:30;Th 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:30-12:45,13:45-18:00;Sa 08:30-13:00',
       'Mo-Fr 08:00-12:00,13:00-16:30',
       'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:30-12:30,13:30-17:30;Sa-Su 09:45-15:00',
       'Mo-Fr 06:30-18:00;Sa 09:00-15:30;Su 11:00-17:00',
       'Mo-Fr 08:00-13:00,13:30-17:00;Sa 08:00-13:00',
       'Mo,Th-Fr 08:00-16:30;Tu-We 08:00-12:30,13:00-17:00;Sa 08:00-12:30,13:00-16:00',
       'Mo,Th-Fr 07:15-11:45,12:00-17:00;Tu-We 07:15-11:45,12:30-17:00;Su 10:00-15:00',
       'Mo 12:30-18:00,8:00-12:00;Tu-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
       'Mo-Fr 07:00-19:00;Sa-Su 08:00-18:00',
       'Mo-Fr 07:00-12:00,14:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 06:30-21:00;Sa 08:00-19:00;Su 09:00-20:00',
       'Mo-Fr 08:00-12:30,13:30-18:00;Sa 09:00-13:00',
       'Mo,We-Fr 07:00-12:45,13:45-17:00;Tu 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:00-12:00,12:45-17:00',
       'Mo-Fr 07:00-19:30;Sa-Su 09:15-17:45',
       'Mo-Fr 08:00-17:00;Sa 08:00-13:00',
       'Mo-Fr 08:00-12:00,13:00-16:00',
       'Mo-Fr 08:00-18:30;Sa 08:30-16:30',
       'Mo-Fr 07:00-18:00;Sa 08:00-13:00',
       'Mo-Fr 05:45-20:30;Sa-Su 06:45-20:00',
       'Mo-Fr 08:00-13:00,14:00-18:00;Sa 08:30-13:00',
       'Mo 06:00-19:30;Tu-Fr 07:00-19:30;Sa 08:00-17:00;Su 09:00-17:30',
       'Mo-Fr 06:00-21:00;Sa-Su 07:00-21:00',
       'Mo 06:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Tu-Th 07:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Fr 07:00-20:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Sa 09:00-17:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Su 10:00-18:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 "',
       'Mo 07:30-12:00,12:45-16:30;Tu-Fr 08:30-12:00,12:45-16:30',
       'Mo-Fr 07:45-12:15,13:00-17:45',
       'Mo-Fr 09:00-12:00,13:00-17:25 open "Bitte Beachten: ;Am Montag, 31.07.23 geschlossen "',
       'Mo-Fr 06:50-12:30,13:15-17:05;Sa 07:30-11:00,11:30-15:30;Su 08:45-12:00,12:30-17:15',
       'Mo-Fr 08:00-18:00;Sa 09:00-13:00',
       'Mo-Fr 08:00-12:00,13:00-17:00',
       'Mo-Fr 06:00-18:00;Sa 07:00-14:00;Su 08:30-16:00',
       'Mo-Fr 08:00-12:00,12:45-17:45;Sa 08:00-13:00',
       'Mo-Fr 08:45-12:00,12:45-17:00',
       'Mo-Fr 08:00-20:00;Sa 10:00-20:00;Su 10:00-18:00',
       'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
       'Mo 07:00-11:30,12:15-16:00;Tu-Fr 08:00-12:15,13:00-16:00',
       'Mo-Fr 08:30-12:15,12:45-16:00',
       'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 10:00-19:00',
       'Mo-Fr 07:00-12:30,13:30-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:30-18:15 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Sa 08:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Su 10:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr"',
       'Mo-Fr 08:30-13:00,14:00-18:00;Sa 08:30-13:00',
       'Mo-Fr 05:45-20:00;Sa 06:30-19:30;Su 08:30-19:30',
       'Mo-Fr 07:30-18:30 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum";Sa 09:00-14:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
       'Mo-Fr 08:00-11:30,12:00-17:00;Sa-Su 08:00-11:30,12:00-15:30',
       'Mo-Fr 07:30-18:30;Sa 07:30-11:30,12:00-15:30;Su 09:30-13:30,14:00-17:30',
       'Mo-Fr 06:00-20:00;Sa 07:00-19:00;Su 08:00-20:00',
       'Mo-Fr 06:00-21:15;Sa 07:00-19:00;Su 08:00-20:00',
       'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:30',
       'Mo-Fr 08:00-19:00;Sa 08:00-16:00;Su 09:00-16:00',
       'Mo-Fr 08:00-12:15,12:45-16:00',
       'Mo-Fr 06:45-18:35;Sa 07:30-13:00',
       'Mo 06:00-10:30,11:30-16:00;Tu,Th-Fr 08:30-12:30,13:30-16:00;We 09:30-12:30,13:30-19:00;Sa 08:00-12:00',
       'Mo-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
       '24/7 closed "Dauerhaft geschlossen"', 'Mo-Fr 06:15-16:40',
       'Mo-Fr 07:00-20:00;Sa-Su 09:00-18:00',
       'Mo-Fr 06:50-20:00;Sa-Su 07:50-19:00',
       'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 08:00-13:00,14:00-18:00',
       'Mo-Fr 07:00-20:00;Sa-Su 08:00-18:00',
       'Mo-Fr 08:00-12:00,13:00-16:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
       'Mo-Fr 06:00-10:30,11:00-15:30,16:00-21:00;Sa-Su 06:45-15:30,16:00-19:45',
       'Mo-We 08:00-12:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Th-Fr 13:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 07:00-17:00;Sa 08:00-13:00',
       'Mo-Fr 09:00-13:15,13:45-17:00',
       'Mo-Fr 07:00-18:30;Sa 08:00-12:00,12:30-16:00;Su 10:00-15:30',
       'Mo,We-Fr 08:00-12:30,13:30-18:00;Tu 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:30-12:20,12:50-16:30',
       'Mo-We,Fr 07:00-12:45,13:45-17:00;Th 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 06:15-12:00,12:45-16:00;Sa 07:45-12:45',
       'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-13:30',
       'Mo-Fr 08:30-12:30,14:00-17:00',
       'Mo,Fr 06:30-19:15 open "gültig ab 01.01.22";Tu-Th 07:30-12:30,13:15-17:15 open "gültig ab 01.01.22";Su 10:45-14:00,14:30-19:00 open "gültig ab 01.01.22"',
       'Mo-Fr 07:00-21:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 09:00-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 10:00-20:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 09:00-12:30,13:00-16:45', 'Mo-Fr 09:10-12:15,13:30-17:50',
       'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 09:00-20:00',
       'Mo-Fr 09:00-12:00,13:00-17:00 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
       'Mo-Fr 09:00-12:30,13:00-18:00;Sa 09:00-13:00,13:30-16:00;Su 12:00-15:30,16:00-20:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:30-19:00;Sa 09:00-18:00;Su 10:00-18:30',
       'Mo-Fr 06:30-18:00;Sa 08:00-17:00;Su 10:00-13:30',
       'Mo-Fr 07:00-18:00;Sa-Su 10:00-15:15',
       'Mo-Fr 08:30-12:00,12:45-16:15 open "Nutzen Sie auch das Video-Reisezentrum am Vorplatz"',
       'Mo-Fr 07:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 09:00-13:00,13:30-17:00;Sa 08:00-13:00',
       'Mo-Fr 06:15-12:00,12:35-16:50;Sa 08:30-14:00;Su 09:30-13:30',
       'Mo-Th 08:00-18:00;Fr 08:00-13:00,14:00-18:00',
       'Mo-Fr 10:00-16:00',
       'Mo-Fr 08:00-12:30,13:15-18:00;Sa-Su 08:45-11:45,12:15-16:15',
       'Mo-Fr 06:45-18:45;Sa 07:45-15:30',
       'Mo-Fr 07:30-18:00;Sa 09:00-14:00;Su 09:30-13:30,14:00-17:00',
       'Mo-Fr 08:15-12:15,13:00-17:45;Sa 07:15-12:30,13:00-15:45',
       'Mo-Fr 07:30-18:30;Sa 08:30-14:00;Su 11:30-16:30',
       'Mo 07:45-12:45,13:45-17:00;Tu,Th-Fr 07:00-12:45,13:45-17:00;We 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 07:30-13:00,14:00-18:00;Sa-Su 08:15-13:00,13:30-16:00',
       'Mo-Fr 06:30-19:00;Sa 08:30-14:05',
       'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-12:30',
       'Mo-Fr 06:30-18:30;Sa 07:30-12:30;Su 11:30-16:30',
       'Mo-Fr 08:00-20:00;Sa-Su 09:00-19:00',
       'Mo-Fr 08:00-20:00;Sa-Su 10:00-20:00',
       'Mo-Fr 07:00-18:00;Sa 08:30-16:00;Su 09:30-17:00',
       'Mo-Fr 06:00-20:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 08:30-13:15,14:15-18:10;Sa 08:30-13:15',
       'Mo-Th 07:15-12:00,12:30-15:45',
       'Mo-Fr 06:00-21:15;Sa-Su 07:00-19:00',
       'Mo-We,Fr 07:30-13:00,14:00-17:00;Th 06:00-15:00,15:30-20:00;Sa 07:30-11:00,11:30-15:00;Su 09:30-15:00',
       'Mo-Fr 07:45-12:45,13:30-18:00;Sa-Su 08:30-12:30,13:00-15:15',
       'Mo-Fr 08:00-18:00 open "Bitte beachten Sie unsere geänderten Öffnungszeit am ;21.07.23 von 08:00-12:30 und 13:00-16:00 Uhr; Wir bedienen Sie auch persönlich im DB Videoreisezentrum "',
       'Mo-Fr 06:30-18:00 open "Wir danken für Ihr Verständnis";Sa 08:30-13:30 open "Wir danken für Ihr Verständnis"',
       'Mo-Fr 08:30-11:30,12:30-16:55 open " Wir bedienen Sie auch persönlich im DB Videoreisezentrum ;am Bahnsteig 1. "',
       'Mo-Fr 06:30-12:10,12:55-16:50;Sa 06:45-12:15',
       'Mo-Fr 08:00-18:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 06:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 07:00-19:00;Sa 08:30-17:45;Su 09:30-18:00',
       'Mo-Fr 08:50-12:30,13:45-17:30',
       'Mo-Tu,Th-Fr 07:30-11:50,12:40-15:40',
       'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 09:30-18:30',
       'Mo-Fr 08:30-12:30,13:45-17:10',
       'Mo-Fr 07:00-19:00;Sa 08:00-17:30;Su 10:00-15:30',
       'Mo-Tu,Th-Fr 08:00-12:30,13:30-18:00;We 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:15-17:30;Sa 09:15-13:00',
       'Mo-Fr 08:00-12:30,13:30-17:30;Sa 08:30-12:30',
       'Mo 06:00-17:30;Tu-Fr 07:30-17:30;Sa 07:00-09:45,10:15-13:30;Su 10:00-17:30',
       'Mo-Fr 06:15-20:15;Sa-Su 08:15-18:15',
       'Mo-Fr 06:00-22:00;Sa-Su 07:00-22:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-18:00',
       'Mo-Fr 07:00-18:30;Sa 08:30-14:00',
       'Mo 08:15-12:30,13:00-16:15;Tu-Fr 08:00-12:30,13:00-15:30;Sa 08:15-13:15'],
      dtype=object)
def getDays(daysList):
    days_of_week = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
    start_index = days_of_week.index(daysList[0])
    end_index = days_of_week.index(daysList[-1])
    
    if start_index <= end_index:
        days_between = days_of_week[start_index:end_index+1]
    else:
        days_between = days_of_week[start_index:] + days_of_week[:end_index+1]
    
    return days_between
def get_minute_intervals(interval, time_range):
    start_str, end_str = time_range.split('-')
    start_time = datetime.strptime(start_str, '%H:%M')
    end_time = datetime.strptime(end_str, '%H:%M')
    
    interval = timedelta(minutes=interval)
    current_time = start_time
    intervals = []
    
    while current_time <= end_time:
        intervals.append(current_time.strftime('%H:%M'))
        current_time += interval
    
    return intervals

Aktuell beinhalten die Daten nur die Öffnungszeiten pro Tag, ohne die Angabe genauer Uhrzeiten. Dazu müsste vermutlich eine doppelte x-Achse verwenden, um sowohl die Tag- als auch die Uhrzeiten anzuzeigen.

data_rows = []
for entry in df_travel_center['openingHours']:
    day_time_pairs = entry.split(';')
    row = {}
    for pair in day_time_pairs:
        try:
            days, times = pair.split(' ')
            days = days.split('-')

            fixed_list = []
            for item in days:
                if (',' in item):
                    day_item = item.split(',')
                    fixed_list.append(day_item)
                else:
                    fixed_list.append(item)
            
            allDays = getDays(fixed_list)
            #print(allDays)
            for day in allDays:
                row[day] = 1
        except Exception as e:
            #print(e)
            #times = pair.split(' ')
            #print(times)
            row[day] = 0
    data_rows.append(row)

df_opening_hours = pd.DataFrame(data_rows)
df_opening_hours
Mo Tu We Th Fr Sa Su
0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1 1.0 1.0 1.0 1.0 1.0 1.0 NaN
2 1.0 1.0 1.0 1.0 1.0 1.0 1.0
3 0.0 NaN NaN 1.0 NaN 1.0 NaN
4 1.0 1.0 1.0 1.0 1.0 1.0 NaN
... ... ... ... ... ... ... ...
255 1.0 1.0 1.0 1.0 1.0 1.0 1.0
256 1.0 1.0 1.0 1.0 1.0 1.0 NaN
257 1.0 1.0 1.0 1.0 1.0 1.0 NaN
258 1.0 1.0 1.0 1.0 1.0 NaN NaN
259 1.0 1.0 1.0 1.0 1.0 1.0 NaN

260 rows × 7 columns

Man kann sehen, dass in der Tabelle häufig NaN auftaucht. Dass die Daten an dieser Stelle nicht verarbeitet werden konnten, lag daran, dass Kommentare in den Zeiten vorhanden waren, wie beispielsweise “Aufgrund von Krankheit geschlossen”.
Daher wird davon ausgegangen, dass NaN ebenfalls geschlossen bedeutet.

df_opening_hours.replace(np.nan, 0, inplace=True)
df_opening_hours
Mo Tu We Th Fr Sa Su
0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1 1.0 1.0 1.0 1.0 1.0 1.0 0.0
2 1.0 1.0 1.0 1.0 1.0 1.0 1.0
3 0.0 0.0 0.0 1.0 0.0 1.0 0.0
4 1.0 1.0 1.0 1.0 1.0 1.0 0.0
... ... ... ... ... ... ... ...
255 1.0 1.0 1.0 1.0 1.0 1.0 1.0
256 1.0 1.0 1.0 1.0 1.0 1.0 0.0
257 1.0 1.0 1.0 1.0 1.0 1.0 0.0
258 1.0 1.0 1.0 1.0 1.0 0.0 0.0
259 1.0 1.0 1.0 1.0 1.0 1.0 0.0

260 rows × 7 columns

Generell ist zu erkennen, dass die meisten Reisezentren am Wochenende, vor allem sonntags, geschlossen haben. Vereinzelt sind Reisezentren aber auch unter der Woche geschlossen, wobei Donnerstag der Tag zu sein scheint, an dem die meisten Reisezentren geöffnet haben.

Bei nährerer Betrachtung sieht man allerdings auch, dass große Knotenpunkte wie Mannheim, München, Hamburg oder Berlin durchgehend geöffnet haben. Hierbei ist die von Plotly mitgelieferte Zoom-Funktion hilfreich.
Leider wird dabei die Schriftgröße nicht ebenfalls gezoomt, daher ist die Anzeige der Y-Axe schwierig. Für eine bessere Lesbarkeit, bietet es sich an, das Overlay nutzen.

custom_color_scale = [
    [0.0, 'rgb(255, 0, 0)'],     # closed = red
    [1.0, 'rgb(0, 255, 0)']      # open = green
]

fig = go.Figure(data=go.Heatmap(
    z=[[col for col in row] for _, row in df_opening_hours.iterrows()],
    x=df_opening_hours.columns,
    y=df_travel_center.iloc[df_opening_hours.index]['name'],
    xgap=15,
    ygap=1,
    colorscale=custom_color_scale,
    hoverongaps=False
    #colorbar_thickness = 10
))

fig.update_layout(
    title='Öffnungszeiten Reisezentren',
    height=750
)

fig.update_xaxes(title_text='Wochentage',tickson='labels')
fig.update_yaxes(visible=False) # , tickfont = dict(size=4)
fig.update_traces(showscale=False)

fig.show()
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Hier lassen sich noch deutlicher die Schließ- und Öffnungszeiten erkennen. Allerdings unterschieden sich Werktage kaum voneinander, sodass es sich hierbei auch um Ungenauigkeiten in den Daten handeln kann.

df_opening_hours_grouped = df_opening_hours.sum().sort_values()
px.bar(df_opening_hours_grouped)
Unable to display output for mime type(s): application/vnd.plotly.v1+json

Nun können noch die Informationen der bahnhofsnahmen Dienstleistungen der Karte hinzugefügt werden.
Dazu müssen die Dataframes zunächst wieder zusammengebracht werden.

df_station_facilities_by_id = df_local_services.groupby('id')['type'].unique().reset_index()
df_station_facilities_by_id.head(1)
id type
0 1 [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW...
df_station_facilities_by_id['id'] = df_station_facilities_by_id['id'].astype(int)
# del map_df_extended
map_df_extended = pd.merge(left=map_df, right=df_station_facilities_by_id, on=['id'], how='left')
map_df_extended.head(1)
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image type
0 1 Aachen DE 2a 50.7678 6.091499 {} Aachen Hbf RB West DB S&S 52064 Nordrhein-Westfalen CATEGORY_2 Bahnhofstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] https://api.railway-stations.org/photos/de/1_1... [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW...

Da in einigen Reihen von type NaN-Werte vorhanden sind, müssen diese zunächst ausgetauscht werden. Immer wenn ein Wert nicht vom Typ Numpy Array ist (also NaN), wird eine neue leere Liste erstellt, um keine Daten zu verlieren.

def replace_nan_with_empty_array(value):
    if isinstance(value, np.ndarray):
        return value
    else:
        return np.array([])
map_df_extended['type'] = map_df_extended['type'].apply(replace_nan_with_empty_array)
map_df_extended
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image type
0 1 Aachen DE 2a 50.767800 6.091499 {} Aachen Hbf RB West DB S&S 52064 Nordrhein-Westfalen CATEGORY_2 Bahnhofstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] https://api.railway-stations.org/photos/de/1_1... [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW...
1 2 Aachen DE 48 50.770202 6.116475 {} Aachen-Rothe Erde RB West DB S&S 52066 Nordrhein-Westfalen CATEGORY_4 Beverstr. [REGIONAL_TRAIN, BUS, CITY_TRAIN] [AAV, VRS] https://api.railway-stations.org/photos/de/2_1... [CAR_PARKING, BICYCLE_PARKING, TRIPLE_S_CENTER...
2 3 Aachen DE 1 50.780360 6.070715 {} Aachen West RB West DB S&S 52072 Nordrhein-Westfalen CATEGORY_5 Republikplatz [REGIONAL_TRAIN, BUS] [AAV, VRS] https://api.railway-stations.org/photos/de/3_1... [TRIPLE_S_CENTER, CAR_PARKING, BICYCLE_PARKING...
3 4 Aalen DE 1 48.841013 10.096271 {} Aalen Hbf RB Südwest DB S&S 73430 Baden-Württemberg CATEGORY_3 Am Bahnhof [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS] [OAM] https://api.railway-stations.org/photos/de/4.jpg [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, CAR_P...
4 5 Abensberg DE 27 48.819456 11.846620 {} Abensberg RB Süd DB S&S 93326 Bayern CATEGORY_6 Bahnhofstr. [REGIONAL_TRAIN] [] https://api.railway-stations.org/photos/de/5_1... [TRIPLE_S_CENTER, PUBLIC_RESTROOM, TAXI_RANK, ...
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
5716 8361 Thalheim DE NaN 50.696652 12.849032 {} Thalheim (Erzgeb) Mitte Erzgebirgsbahn (EGB) DB Regio-Netze 09380 Sachsen CATEGORY_6 Salzstraße [REGIONAL_TRAIN, BUS] [VMS] NaN []
5717 8363 Herten DE 35 51.597508 7.139053 {} Herten (Westf) RB West DB S&S 45699 Nordrhein-Westfalen CATEGORY_5 Gartenstr. [BUS, CITY_TRAIN] [VRR] NaN []
5718 8375 Rövershagen DE 41 54.160000 12.238300 {} Rövershagen Karls Erlebnisdorf (Purkshof) RB Ost DB S&S 18182 Mecklenburg-Vorpommern CATEGORY_7 Purkshof [REGIONAL_TRAIN] [VVW] NaN []
5719 8388 Eutingen/Gäu DE 20 48.484700 8.753100 {} Eutingen Nord RB Südwest DB S&S 72184 Baden-Württemberg CATEGORY_6 Göttelinger Str. [REGIONAL_TRAIN, BUS, CITY_TRAIN] [VGF] NaN []
5720 8459 Düsseldorf DE 120 51.278517 6.766979 {} Düsseldorf Flughafen Terminal RB West DB S&S 40474 Nordrhein-Westfalen CATEGORY_4 Flughafenstraße [REGIONAL_TRAIN, CITY_TRAIN] [VRS, VRR] NaN []

5721 rows × 18 columns

Jetzt wird wieder für jeden Typ eine FeatureGroup für die Karte erstellt.

for val in map_df_extended['type'][0]:
    print(val)
MOBILE_TRAVEL_SERVICE
TRIPLE_S_CENTER
RAILWAY_MISSION
HANDICAPPED_TRAVELLER_SERVICE
LOCKER
WIFI
CAR_PARKING
BICYCLE_PARKING
PUBLIC_RESTROOM
TRAVEL_NECESSITIES
TAXI_RANK
TRAVEL_CENTER
LOST_PROPERTY_OFFICE
facilities_dict = {}
for rowIndex in map_df_extended.index:
    for listValue in map_df_extended['type'][rowIndex]:
        facilities_dict.setdefault(listValue, folium.FeatureGroup(name=listValue, show=False, autoZIndex=False))
facilities_dict
{'MOBILE_TRAVEL_SERVICE': <folium.map.FeatureGroup at 0x230416727d0>,
 'TRIPLE_S_CENTER': <folium.map.FeatureGroup at 0x23041673250>,
 'RAILWAY_MISSION': <folium.map.FeatureGroup at 0x23041672e30>,
 'HANDICAPPED_TRAVELLER_SERVICE': <folium.map.FeatureGroup at 0x23041672d10>,
 'LOCKER': <folium.map.FeatureGroup at 0x23041672e60>,
 'WIFI': <folium.map.FeatureGroup at 0x23041672fb0>,
 'CAR_PARKING': <folium.map.FeatureGroup at 0x23041672ef0>,
 'BICYCLE_PARKING': <folium.map.FeatureGroup at 0x230416730a0>,
 'PUBLIC_RESTROOM': <folium.map.FeatureGroup at 0x230416728c0>,
 'TRAVEL_NECESSITIES': <folium.map.FeatureGroup at 0x23041673700>,
 'TAXI_RANK': <folium.map.FeatureGroup at 0x230416736d0>,
 'TRAVEL_CENTER': <folium.map.FeatureGroup at 0x230416736a0>,
 'LOST_PROPERTY_OFFICE': <folium.map.FeatureGroup at 0x23041673670>,
 'RAD_PLUS': <folium.map.FeatureGroup at 0x23041673640>,
 'INFORMATION_COUNTER': <folium.map.FeatureGroup at 0x23041673610>,
 'VIDEO_TRAVEL_CENTER': <folium.map.FeatureGroup at 0x23041673160>,
 'CAR_RENTAL': <folium.map.FeatureGroup at 0x230416726b0>,
 'TRAVEL_LOUNGE': <folium.map.FeatureGroup at 0x230416730d0>}
# NOTE: This requires python 3.10.1
def GetIconForFacilities(facility):
    try:
        match facility:
            case 'MOBILE_TRAVEL_SERVICE':
                return folium.Icon(color='lightblue', icon='map-marker')
            case 'TRIPLE_S_CENTER':
                return folium.Icon(color='red', icon='map-marker')
            case 'RAILWAY_MISSION':
                return folium.Icon(color='darkpurple', icon='map-marker')
            case 'HANDICAPPED_TRAVELLER_SERVICE':
                return folium.Icon(color='lightgray', icon='map-marker')
            case 'CAR_PARKING':
                return folium.Icon(color='gray', icon='map-marker')
            case 'PUBLIC_RESTROOM':
                return folium.Icon(color='cadetblue', icon='map-marker')
            case 'TAXI_RANK':
                return folium.Icon(color='beige', icon='map-marker')
            case 'LOST_PROPERTY_OFFICE':
                return folium.Icon(color='white', icon='map-marker')
            case 'CAR_RENTAL':
                return folium.Icon(color='black', icon='map-marker')
            case _:
                return folium.Icon(color='blue', icon='map-marker')
    except:
        return folium.Icon(color='blue', icon='map-marker')

Letztlich werden beide Feature Groups, die der Bundesländer und die der Einrichtungen, zusammen auf die Karte gebracht.

Erst werden alle Marker den Feature Groups hinzugefügt, anschließend werden die Feature Groups der Karte hinzugefügt.

Hieraus ergibt sich das Problem, dass sich aufgrund der gleichen Marker an der gleichen Lokation Überschneidungen ergeben. Darüber hinaus kann das Laden je nach Computerressourcen länger dauern oder die Karte nicht korrekt gerendert werden.

Um diesem Problem entgegenzuwirken, kann das Folium Plugin MarkerCluster genutzt werden, welches nahe Marker gruppiert.
Zoomt man in die Karte, werden diese aufgelöst. Sind Marker auf der exakt gleichen Stelle, können durch einen Klick darauf alle Marker betrachtet werden.

Auf diese Weise lassen sich performant alle Marker gleichzeitig auf der Karte anzeigen.

Besonders viele und damit performance-intensive Werte sind TRIPE_S_CENTER, CAR_PARKING und BYICYLE_PARKING. Es gibt aber auch viele PUBLIC_RESTROOM, TRAVEL_NECESSITIES und TAXI_RANK.

map_df = map_df_extended
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.

cluster = plugins.MarkerCluster(name='Deutschland')
cluster.add_to(m)

for i in map_df.index:
    html=f"""
    <img src="{map_df['image'][i]}" width="500px">
    <br/>
    <b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
    <p>Transports: {map_df['availableTransports'][i]}</p>
    <p>Associations: {map_df['transportAssociations'][i]}</p>
    <p>Local services: {map_df['type'][i]}</p>
    """

    parsedHtml = folium.Html(html, script=True)
    popup = folium.Popup(parsedHtml, max_width=2650)

    feature_group_state = state_dict[map_df['state'][i]]
    marker = folium.Marker(
            location=[ map_df['latitude'][i], map_df['longitude'][i] ], 
            icon=GetIcon(map_df['availableTransports'][i]),
            radius=8,
            tooltip=map_df['name'][i],
            popup=popup
        )
    
    marker.add_to(feature_group_state)
    marker.add_to(cluster)

    for listValue in map_df_extended['type'][i]:
        feature_group_type = facilities_dict[listValue]
        marker = folium.Marker(
            location=[ map_df['latitude'][i], map_df['longitude'][i] ], 
            icon=GetIconForFacilities(listValue),
            radius=8,
            tooltip=map_df['name'][i] + ' - ' + listValue
        )
        
        marker.add_to(feature_group_type)
        marker.add_to(cluster)
for fg in state_dict.values():
    m.add_child(fg)

for fg in facilities_dict.values():
    m.add_child(fg)

folium.LayerControl(collapsed=False).add_to(m)
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Die Karte kann auch als HTML gespeichert werden.

m.save("./db_map.html")

Fazit

Es ist schon unglaublich, was die Deutsche Bahn alles leistet. Dazu gehören nicht nur fahrende Züge, sondern die komplette Mobilitätsinfrastruktur. Der lukrative Bereich Güterverkehr wurde dabei nicht einmal betrachtet.

Wie es wirklich um die Bahn bestellt ist, lässt sich somit nicht anhand einer Frage und Antwort bewerten. Allerdings bekommt man durch die Daten einen Eindruck davon, mit welcher, zum Teil sicherlich auch selbst verschuldeten, Komplexität das Unternehmen Bahn umgehen muss.

Dies zeigen allein die 5670 Bahnhöfe, von denen die meisten noch weitere Services wie Reiseauskunft, Toiletten, Auto- und Fahrradparkplätze und Schließfächer anbieten.
Die Bahn ist aber auch an vielen Orten für diejenigen da, die Hilfe benötigen, sei es in der Bahnhofsmission oder mit dem Service für barrierefreies Reisen.
Hinzu kommen relativ neue Anforderungen wie W-LAN oder einen Ort, bei dem man während des Wartens produktiv sein kann, wie beispielsweise die DB Lounge.

Schwerer tut man sich bei den 49 Verkehrsverbünden mit jeweils eigenen Tickets und 12 Organisationseinheiten. Hier ist mit dem Deutschlandticket, zumindest von außen betrachtet, ein großer Schritt gelungen.

Gerade als Ferienurlauber würde man sich wünschen, dass die Verfügbarkeit der Reisezentren, insbesondere an den Wochenenden, besser wäre. Allerdings trifft dies häufig nur für kleinere Bahnhöfe zu und stellt somit eher Einzelfälle dar.

Der Zustand der Aufzüge und Rolltreppen ist leider schlecht, was man selbst an der Haltestelle Universität in Stuttgart schon häufiger zu spüren bekommen hat.
Da gerade Menschen, die in ihrer Mobilität eingeschränkt sind, auf Aufzüge angewiesen sind, sollte die Bahn hier dringend nachbessern.


Die Bahn ist ein extrem vielschichtiges Unternehmen, welches, wie jedes andere Unternehmen in dieser Größenordnung, mit Problemen konfrontiert ist. Die Auswirkungen dieser Probleme bekommen allerdings täglich die reisenden Kunden unmittelbar zu spüren.
Verspätungen und Zugausfälle können erhebliche Unannehmlichkeiten verursachen und das Vertrauen der Fahrgäste in den Service beeinträchtigen. Dies kann wiederum Auswirkungen auf die Gesamtwahrnehmung und den Erfolg der Deutschen Bahn haben.

Die Bahn muss einer Reihe von Anforderungen und Erwartungen gerecht werden. Trotz aller Kritik und aufgezeigter Probleme leistet die Bahn tagtäglich einen bedeutenden Beitrag zur Mobilität in Deutschland.